feat(ai): add type-safe tool call events to chat() stream#452
feat(ai): add type-safe tool call events to chat() stream#452AlemTuzlak wants to merge 13 commits into
Conversation
When tools with Zod schemas are passed to chat(), the stream chunks now carry type information on TOOL_CALL_START and TOOL_CALL_END events: - toolName narrows to the union of tool name literals - input on TOOL_CALL_END is typed as the union of tool input types Made ToolCallStartEvent and ToolCallEndEvent generic with backward- compatible defaults. Added TypedStreamChunk<TTools> type that threads through TextActivityOptions, TextActivityResult, chat(), and createChatOptions(). Includes IsAny guard in ToolInputsOf to prevent `any` leaking through InferSchemaType for tools without inputSchema. Fully backward compatible — StreamChunk and AGUIEvent are unchanged, unparameterized event types use string/unknown defaults.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe changes introduce type-safe streaming for tool calls in the chat function. When Changes
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Changeset Version Preview1 package(s) bumped directly, 0 bumped as dependents. 🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit 2b0bd96
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-anthropic
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/chat/streaming.md`:
- Around line 115-117: Remove the blank line between the two blockquotes so the
"**Note:**" and "**Tip:**" paragraphs are adjacent (or merge them into one
blockquote); specifically edit the section containing the "**Note:** When
multiple tools..." and the "**Tip:** The typed stream chunk type..." strings so
there is no empty line between those blockquote lines to satisfy markdownlint
MD028.
In `@docs/reference/type-aliases/StreamChunk.md`:
- Line 23: The "Defined in" source anchor for the TypedStreamChunk entry is
stale; update the anchor in docs/reference/type-aliases/StreamChunk.md to point
to the current declaration location packages/typescript/ai/src/types.ts at line
1048 (or to the exact URL/permalink generated by TypeDoc for TypedStreamChunk),
ensuring the link text and URL reflect the new file and line; update the
markdown so it uses the correct Markdown link format and matches the repo's
TypeDoc-generated anchor for TypedStreamChunk.
In `@packages/typescript/ai/tests/type-check.test.ts`:
- Around line 6-17: The named imports in this test file are not alphabetized
causing ESLint sort-imports errors; reorder the members inside each import's
braces alphabetically (e.g., change "describe, it, expectTypeOf" to "describe,
expectTypeOf, it" for the vitest import, and similarly alphabetize "chat,
createChatOptions, toolDefinition" and the type import list "JSONSchema,
StreamChunk, Tool, ToolCallArgsEvent, ToolCallEndEvent, ToolCallStartEvent,
TypedStreamChunk") so each import's named members are in ascending alphabetical
order.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 28ddc119-4e8d-45ba-8567-1aae00525797
📒 Files selected for processing (7)
docs/chat/streaming.mddocs/reference/type-aliases/StreamChunk.mddocs/tools/tools.mdexamples/ts-react-chat/src/routes/api.tanchat.tspackages/typescript/ai/src/activities/chat/index.tspackages/typescript/ai/src/types.tspackages/typescript/ai/tests/type-check.test.ts
| import { describe, it, expectTypeOf } from 'vitest' | ||
| import { createChatOptions } from '../src' | ||
| import { z } from 'zod' | ||
| import { chat, createChatOptions, toolDefinition } from '../src' | ||
| import type { | ||
| JSONSchema, | ||
| StreamChunk, | ||
| Tool, | ||
| ToolCallArgsEvent, | ||
| ToolCallStartEvent, | ||
| ToolCallEndEvent, | ||
| TypedStreamChunk, | ||
| } from '../src' |
There was a problem hiding this comment.
Fix the import member ordering before merge.
ESLint is already flagging this block (sort-imports on Line 6 and Line 15), so this file will stay red until the named imports are alphabetized.
🧰 Tools
🪛 ESLint
[error] 6-6: Member 'expectTypeOf' of the import declaration should be sorted alphabetically.
(sort-imports)
[error] 15-15: Member 'ToolCallEndEvent' of the import declaration should be sorted alphabetically.
(sort-imports)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai/tests/type-check.test.ts` around lines 6 - 17, The
named imports in this test file are not alphabetized causing ESLint sort-imports
errors; reorder the members inside each import's braces alphabetically (e.g.,
change "describe, it, expectTypeOf" to "describe, expectTypeOf, it" for the
vitest import, and similarly alphabetize "chat, createChatOptions,
toolDefinition" and the type import list "JSONSchema, StreamChunk, Tool,
ToolCallArgsEvent, ToolCallEndEvent, ToolCallStartEvent, TypedStreamChunk") so
each import's named members are in ascending alphabetical order.
…put narrowing Replace flat toolName/input unions with distributive conditional types so checking toolName === 'x' narrows input to that specific tool's type.
There was a problem hiding this comment.
♻️ Duplicate comments (2)
docs/reference/type-aliases/StreamChunk.md (1)
23-23:⚠️ Potential issue | 🟡 MinorStale
Defined inlink forTypedStreamChunk.The link points to
types.ts:1033, butTypedStreamChunkis defined at line 1065 in the current code. This will create a broken anchor.📝 Suggested fix
-Defined in: [types.ts:1033](https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/types.ts#L1033) +Defined in: [types.ts:1065](https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/types.ts#L1065)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/reference/type-aliases/StreamChunk.md` at line 23, The documentation entry for TypedStreamChunk has a stale "Defined in" anchor; update the link target in docs/reference/type-aliases/StreamChunk.md so it points to the actual definition of TypedStreamChunk in types.ts (current location around line 1065) or replace the fragile line-number anchor with a file-level link to types.ts and a search-friendly fragment for TypedStreamChunk; ensure the reference text "Defined in" continues to reference types.ts and that the anchor resolves to the TypedStreamChunk definition.packages/typescript/ai/tests/type-check.test.ts (1)
6-17:⚠️ Potential issue | 🟡 MinorImport members need alphabetical sorting.
ESLint's
sort-importsrule requires alphabetically ordered named imports within each statement.🔧 Suggested fix
-import { describe, it, expectTypeOf } from 'vitest' +import { describe, expectTypeOf, it } from 'vitest' import { z } from 'zod' import { chat, createChatOptions, toolDefinition } from '../src' import type { JSONSchema, StreamChunk, Tool, ToolCallArgsEvent, - ToolCallStartEvent, ToolCallEndEvent, + ToolCallStartEvent, TypedStreamChunk, } from '../src'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai/tests/type-check.test.ts` around lines 6 - 17, The named imports in this file are not alphabetized; reorder the identifiers alphabetically within each import statement—for example change "describe, it, expectTypeOf" to "describe, expectTypeOf, it", "chat, createChatOptions, toolDefinition" to "chat, createChatOptions, toolDefinition" (ensure correct alphabetical order), and the type import list "JSONSchema, StreamChunk, Tool, ToolCallArgsEvent, ToolCallStartEvent, ToolCallEndEvent, TypedStreamChunk" should be reordered alphabetically (e.g., "JSONSchema, StreamChunk, Tool, ToolCallArgsEvent, ToolCallEndEvent, ToolCallStartEvent, TypedStreamChunk") so each import's named members follow ESLint's sort-imports rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@docs/reference/type-aliases/StreamChunk.md`:
- Line 23: The documentation entry for TypedStreamChunk has a stale "Defined in"
anchor; update the link target in docs/reference/type-aliases/StreamChunk.md so
it points to the actual definition of TypedStreamChunk in types.ts (current
location around line 1065) or replace the fragile line-number anchor with a
file-level link to types.ts and a search-friendly fragment for TypedStreamChunk;
ensure the reference text "Defined in" continues to reference types.ts and that
the anchor resolves to the TypedStreamChunk definition.
In `@packages/typescript/ai/tests/type-check.test.ts`:
- Around line 6-17: The named imports in this file are not alphabetized; reorder
the identifiers alphabetically within each import statement—for example change
"describe, it, expectTypeOf" to "describe, expectTypeOf, it", "chat,
createChatOptions, toolDefinition" to "chat, createChatOptions, toolDefinition"
(ensure correct alphabetical order), and the type import list "JSONSchema,
StreamChunk, Tool, ToolCallArgsEvent, ToolCallStartEvent, ToolCallEndEvent,
TypedStreamChunk" should be reordered alphabetically (e.g., "JSONSchema,
StreamChunk, Tool, ToolCallArgsEvent, ToolCallEndEvent, ToolCallStartEvent,
TypedStreamChunk") so each import's named members follow ESLint's sort-imports
rule.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 230ab7f9-f926-49b5-a973-0080ff8b4502
📒 Files selected for processing (5)
docs/chat/streaming.mddocs/reference/type-aliases/StreamChunk.mdexamples/ts-react-chat/src/routes/api.tanchat.tspackages/typescript/ai/src/types.tspackages/typescript/ai/tests/type-check.test.ts
✅ Files skipped from review due to trivial changes (1)
- docs/chat/streaming.md
- Show practical property access after discriminated narrowing - Add description field to searchTool example for consistency - Add cross-link from server-tools to streaming type safety - Fix stale line number references in StreamChunk.md
- Remove T & Tool<any,any,any> intersection in DistributedToolCallEnd that caused tsc to resolve input as unknown for all tools - Relax SafeToolInput constraint to structural match (no generic bound) - Fix "Zod schema inference" → "Standard Schema inference" in JSDoc/docs - Fix off-by-one line number in StreamChunk.md reference - Fix misleading test comment about searchClientTool - Add | undefined to input type annotation in streaming docs - Broaden server-tools.md tip to cover all typed tool variants - Add tests for mixed Zod+JSON Schema and chat() with server/client tools
…xploring-wall # Conflicts: # docs/reference/type-aliases/StreamChunk.md # packages/typescript/ai/src/activities/chat/index.ts # packages/typescript/ai/src/types.ts
Main's upstream bump to `@ag-ui/core` switched the AG-UI event interfaces
to `z.infer<...>` of a `"passthrough"` `z.ZodObject`, which introduces an
`[k: string]: unknown` index signature. `ToolCallStartEvent<TToolName>` was
declared as `Omit<AGUIToolCallStartEvent, 'toolCallName'> & { toolCallName: TToolName; ... }`
— and `Omit` on an index-signature'd type collapses the re-declared field
in ways that destroy discriminated-union narrowing on `StreamChunk`.
Consequences before this change:
- `Extract<StreamChunk, { type: 'TOOL_CALL_START' }>['toolName']` resolved
to `never`, so every type-check test around `TypedStreamChunk` failed.
- `switch (chunk.type) { case 'TOOL_CALL_START': ... }` in `chat/index.ts`
and `handleToolCallStartEvent`'s `Extract<..., {type:'TOOL_CALL_START'}>`
parameter couldn't narrow, cascading 60 tsc errors through
`activities/chat/**`, `middlewares/content-guard.ts`, and tests.
Redesign (types.ts):
- `ToolCallStartEvent<TToolName>` now `extends AGUIToolCallStartEvent`
with **no `Omit`**. The AG-UI `toolCallName: string` is inherited
verbatim on the base interface; narrowing to a literal happens purely
through intersection in the per-tool variants.
- `ToolCallEndEvent<TToolName, TInput>` extends cleanly the same way.
`toolName` is kept required (matches pre-merge TanStack surface and
every adapter emits it).
- `HasTypedTools<TTools>` now partitions out `ProviderTool` before
checking `string extends ... ['name']`. Provider tools carry opaque
provider metadata with a generic `string` name — without filtering,
a user passing `[webSearchTool, myTypedTool]` would silently fall
through to the untyped branch.
- `DistributedToolCallStart` / `DistributedToolCallEnd` now distribute
over `NonProviderTools<TTools>` and match any tool-like shape via
`T extends { name: infer TName extends string }` — picking up
`Tool`, `ServerTool`, and `ClientTool` uniformly.
- The `ProviderTool` partition uses a structural brand match
(`{ readonly '~provider': string; readonly '~toolKind': string }`)
to avoid a circular import between `types.ts` and `./tools/provider-tool.ts`.
Test fix (tests/tool-calls-null-input.test.ts):
- Two fixture calls to `manager.completeToolCall(...)` now include
`toolCallName` / `toolName`. The runtime type requires them; the
pre-merge test relied on the optional-ness accidentally introduced
by the Omit-based surface.
Minor bucket from the review:
- docs/chat/streaming.md: replace the hallucinated `gpt-5.2` model id
with `gpt-4o` in all four occurrences (lines 27, 49, 109, 134).
- examples/ts-react-chat/src/routes/api.tanchat.ts: remove the dead
`typedStreamShowcase` function (`void typedStreamShowcase`).
- packages/typescript/ai-svelte/src/create-chat.svelte.ts: wrap
`onResponse`, `onChunk`, and `onCustomEvent` the same way
`onFinish`/`onError` already were, so callers can mutate the
`options` object and propagate new callbacks (matches the
React/Preact/Vue/Solid sibling wrappers). Comment explains why.
- Add `.changeset/svelte-callback-propagation.md` (patch bump).
Verification:
- `pnpm --filter @tanstack/ai test:types` emits zero errors.
- `pnpm --filter @tanstack/ai test:lib` 735/735 passing.
- `pnpm --filter @tanstack/ai-openai test:lib` 131/131 passing.
- `pnpm --filter @tanstack/ai-anthropic test:lib` 62/62 passing.
- `pnpm --filter @tanstack/ai-svelte test:lib` 53/53 passing.
- `pnpm --filter ... test:types` green across the same four packages.
c798ae0 to
f2aaac0
Compare
Resolves conflicts after main brought in ProviderTool refactor (provider-tool.ts), metadata field rename on ToolCallStartEvent, TStream default flip (issue #526), and the conditional-spread pattern in ai-svelte's createChat. Also extends TypedStreamChunk with a TaggedCustomEvent union so CUSTOM events (structured-output.*, approval-requested, tool-input-available) narrow by name to a typed value when typed tools are present.
Bucket (a) fixes from a 7-agent unbiased review:
1. ai-svelte createChat: restore awaitable onResponse contract
- Was: `void options.onResponse?.(response)` discarded the Promise
- Now: `(response) => options.onResponse?.(response)` returns it
- ChatClient awaits onResponse() (chat-client.ts:611) — voiding broke
the await and let async user work race with stream cleanup. Matches
Vue/Solid/Preact pattern; the void was a lint-silencer mistake
introduced during the main-merge resolution.
- Call sites: 1 (the wrapper itself, rewritten). The wrapper's callee
contract is `(response?: Response) => void | Promise<void>` —
returning the Promise still satisfies it.
2. docs: split TypedStreamChunk into its own reference page
- StreamChunk.md previously held two `# Type Alias:` H1s under a single
id/title, breaking the per-alias-file convention and leaving the
TypedStreamChunk type signature truncated (no `= ...` body).
- New TypedStreamChunk.md includes the full conditional-type body and
replaces the invalid `inputSchema: /* Zod schema */` placeholder with
real Zod expressions.
- StreamChunk.md trimmed back to one alias plus a cross-link.
3. type-check tests: guard NonProviderTools partitioning
- Added a `fakeProviderTool` fixture and two regression tests:
- ProviderTool-only arrays fall back to untyped events
- Mixed [ProviderTool, typed tool] arrays still narrow the typed tool
- Without these, a regression in HasTypedTools that fails to strip
ProviderTool would silently widen toolName back to string.
Verification:
- pnpm --filter @tanstack/ai test:types: green
- pnpm --filter @tanstack/ai-svelte test:types: green
- pnpm --filter @tanstack/ai test:lib: 940 passed (+2 new)
- pnpm --filter @tanstack/ai-svelte test:eslint: 0 errors
The new TypedStreamChunk reference page introduced in 2b0bd96 links to ./TaggedCustomEvent, but the corresponding page didn't exist yet — CI's verify-links script flagged it as broken. Materialize the page manually (TypeDoc would generate it on the next docs regen) so the cross-link resolves and consumers landing on TypedStreamChunk can follow through to the tagged-CUSTOM-event union description. The page documents the four tagged variants (structured-output.*, approval-requested, tool-input-available), the T parameterization on StructuredOutputCompleteEvent, and the user-emitted-event caveat that keeps the bare CustomEvent out of the union to avoid value: any poisoning. Verification: - node scripts/verify-links.ts: green (0 broken links across 299 files) Call-site enumeration: - TypedStreamChunk.md:33 — './TaggedCustomEvent' link now resolves. - StreamChunk.md, no link to TaggedCustomEvent (unchanged). - No other files reference the page.
Round-2 CR confirmation flagged that the JSDoc claimed 'tagged custom events still narrow' in the no-typed-tools branch, but the implementation falls back to plain StreamChunk (no TaggedCustomEvent union) for back-compat with AsyncIterable<StreamChunk> consumers. Consumers reading the JSDoc would write code assuming tagged narrowing on bare chat() calls and find runtime CustomEvent shapes that don't match the type-narrowed expectation. Rewrite the fallback paragraph to state the actual behavior: plain StreamChunk fallback, opt into typed tools to get TaggedCustomEvent narrowing alongside per-tool TOOL_CALL_*. Call-site enumeration: - types.ts:1571 TypedStreamChunk — JSDoc only, no signature change. - Implementation at 1576-1587 is unchanged; verify-types green. - docs/reference/type-aliases/TypedStreamChunk.md already accurately describes the fallback (line 31 'falls back to plain StreamChunk') so the reference doc and source-of-truth comment are now consistent. Verification: - pnpm --filter @tanstack/ai test:types: green - node scripts/verify-links.ts: 0 broken links
Round-3 CR confirmation flagged that the streaming-structured branch of chat() cast its return to TextActivityResult<TSchema, TStream> while the other three branches use TextActivityResult<TSchema, TStream, TTools>. The cast is type-equivalent today (the structured-stream branch of TextActivityResult doesn't reference TTools) but the visual asymmetry is a future-rot hazard — a later refactor that does parameterize the structured branch on TTools would silently miss this site. Call-site enumeration: - chat/index.ts:2161 — the cast itself, updated. - No other call sites cast to TextActivityResult; all consumers receive the return value from chat()'s narrowed conditional type. Verification: - pnpm --filter @tanstack/ai test:types: green - node scripts/verify-links.ts: 0 broken links
Summary
ToolCallStartEventandToolCallEndEventgeneric with backward-compatible defaults (string/unknown)TypedStreamChunk<TTools>exported type that replaces untyped tool call events with typed versions based on the tools arrayTToolsgeneric throughTextActivityOptions,TextActivityResult,chat(), andcreateChatOptions()so the stream return type carries tool type informationIsAnyguard inToolInputsOfto preventanyleaking throughInferSchemaTypefor tools withoutinputSchemachat()inference without explicit type argsStreamChunk.mdreference, and cross-links from tools docsts-react-chatexampleTest plan
pnpm --filter @tanstack/ai test:lib)pnpm --filter @tanstack/ai build)pnpm --filter ts-react-chat build)string/unknown, empty arrays,as const, server/client tool variants,chat()inference, backward compat (assignability,StreamChunkunchanged)toBeUnknown()assertions used instead oftoEqualTypeOf<unknown>()to catchanyleaks🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
toolNamenarrowed to tool name literals andinputtyped to corresponding tool schemas.Bug Fixes
Documentation